Authentication - 各类登录认证代码实现与整合
用户名密码认证
主要逻辑及时序图
前后端分离架构下,基于 Token(JWT)认证的用户名密码登录核心逻辑可以概括为 “两次请求,三重防护,阅后即焚,无状态颁发”。
- 阶段一:预登录(安全铺垫)。前端先向后端发起请求。后端生成一个唯一凭证 qpKey,并以此为键,将图形验证码和动态生成的 RSA 私钥存入 Redis 缓存;随后将验证码图片、RSA 公钥及 qpKey(quick public key) 返回给前端。
- 阶段二:前端加密与提交。用户输入账号密码并填写验证码。前端利用拿到的 RSA 公钥将明文密码加密成密文,连同验证码、qpKey 一并打包提交给后端。
- 阶段三:后端多重校验与解密(阅后即焚)
- 网关/频率限制:首先通过 Redis 计数器进行接口限流或错误次数检查,防止暴力破解。
- 验证码比对:根据 qpKey 从 Redis 捞出验证码进行一致性校验。
- 私钥解密(核心防御):根据 qpKey 捞出 RSA 私钥解密密码,无论对错立刻从 Redis 中删除该私钥与验证码,实现凭证 “阅后即焚”,彻底封杀重放攻击。
- 阶段四:数据库验证与 Token 颁发(无状态)。后端从库中捞出用户信息,查询该用户的 60 位 BCrypt 密文密码。利用 passwordEncoder.matches() 将还原的明文与数据库密文进行慢哈希比对。验证通过后,服务器不再开辟 Session,而是直接签发无状态的 双 Token(Access Token + Refresh Token)返回给前端,至此天生免疫 CSRF 攻击。

代码实现
依赖配置
1 | <dependencies> |
配置文件
aplication.yml
1 | server: |
建表语句
1 | CREATE TABLE `sys_user` |
启动类
1 | import org.springframework.boot.SpringApplication; |
配置类和jwt过滤器
SecurityConfig
1 | import org.springframework.context.annotation.Bean; |
JwtAuthenticationFilter
1 | import io.jsonwebtoken.Claims; |
认证业务类
AuthController
1 | import cn.hutool.captcha.CaptchaUtil; |
UserRepository
1 |
|
UserEntity
1 |
|
LoginDTO
1 |
|
工具类
RsaUtil
1 | import javax.crypto.Cipher; |
JwtUtil
1 | import io.jsonwebtoken.Claims; |
测试验证
1 | # 1 |
手机验证码认证
主要逻辑和时序图
这里为了省钱和方便测试,索性就直接使用邮箱验证码代替手机验证码了。手机验证码认证的时序图如下:

代码实现
依赖配置
在上述基础上,增加依赖项:
1 | <dependency> |
配置文件
在原来基础上增加email配置:
1 | spring: |
认证业务类
AuthEmailController
1 | import jakarta.annotation.Resource; |
测试验证
1 | # 1 |
手机扫码认证登录
主要逻辑和时序图
手机扫码登录(如微信扫码)的本质是:利用 “已经登录且具备安全认证的手机端(App)”,去帮 “处于未登录状态的网页端(PC)” 进行背书和授权。那么,如何证明扫码的人就是用户本人呢?核心就在于:手机端在扫码时,必须把手机本地缓存的、代表用户身份的 Token(凭证)与二维码的唯一标识(SceneKey/UUID)在后端进行绑定。

为了讲透这个逻辑,我们把整个扫码过程拆解为三个步骤,看看安全防线是如何层层递进的。
- 网页端:生成 “带有数字签名” 的无主二维码。当用户打开 PC 端登录页时,网页端向后端请求一个二维码。
- 后端会生成一个全球唯一的临时流水号 qrCodeId(通常是一个 UUID)。
- 后端把这个 qrCodeId 存入 Redis,状态标记为 NOT_SCAN(未扫码),有效期一般为 2~5 分钟。
- 证明的第一步:此时的二维码在全局是唯一的,它就像一个等待失主认领的广播箱。
- 手机端:扫码并进行 “身份核验”。这一步是证明本人的最核心卡点。
- 用户掏出手机,打开已经登录了账号的 App 或小程序去扫描这个二维码。
- 当用户在手机上点击 “确认登录” 时,手机端会向后端发起一个授权请求,这个请求的 Headers 中必然自动携带了手机端本地的 Authorization: Bearer手机端TokenXxx,同时请求体里带着 qrCodeId。App 扫码后, 拿到的就是 qrCodeId。
- 后端网关拦截到手机端的请求后,首先去解析手机端的 Token。因为这个 Token 是用户之前输入密码或指纹成功后颁发的,只要 Token 没过期、没被篡改,后端就能 100% 确认当前发送请求的手机主人是谁(获取到了 userId)。
- 后端:绑定双端凭证(身份转移)
- 后端在确认了手机端用户的 userId 后,立刻去 Redis 里找到对应的 qrCodeId,执行绑定:将 Redis 中该 qrCodeId 的状态修改为 CONFIRMED(已确认授权),将该 qrCodeId 的 Value 绑定上刚刚解析出来的 userId。
- 此时,PC 端的那个 “无主二维码”,就正式和手机端的 “特定用户” 绑定在了一起。PC 端轮询(或通过 WebSocket 监听到)状态变成 CONFIRMED 后,后端直接根据绑定的 userId 为 PC 端签发属于 PC 端的 JWT Token。
如果我们要把这个方案推向生产,只靠上面的逻辑还不够,还必须加上以下三道防线:
- 防跨时空重放:二维码的 qrCodeId 必须具备短暂的有效期(如 2 分钟)。一旦过期,Redis 直接销毁。如果黑客打印了你的二维码去骗别人扫,只要超过 2 分钟就会彻底失效。
- 防调包攻击(二次确认):当手机 App 扫描成功后,App 界面上必须展示出必要的信息(例如:“您正在登录北京时间的 Mac 端浏览器,若非本人操作请拒绝”)。通过这种视觉确认,防止用户在不知情的情况下帮黑客的网页授权。
- 一次性票据与阅后即焚:网页端在监听到状态为 CONFIRMED 成功拿到 PC Token 的那一瞬间,后端必须立刻从 Redis 中把这个 qrCodeId 删掉。确保这个二维码这辈子只能被成功登录一次,彻底封杀任何抓包复用的可能性。
多方式认证的整合
整体架构图
在实际的项目中,一个系统往往需要同时支持用户名密码、邮箱验证码、手机快捷登录、第三方授权(微信/GitHub)、扫码登录等多种认证方式。如果为每种登录方式都写一套独立的拦截器或控制器,代码很快就会变成一盘难以维护的散沙。一般组织多种认证方式的核心思想是:“解耦认证源,统一上下游”(尤其是基于 Spring Security 的企业级架构中)。
Spring Security 采用的是高度抽象的策略模式。它把整个认证链路拆解为了三个最核心的积木:
- Authentication(认证凭证):一个纯粹的承载数据的 “无知” 对象(纸质表单)。
- AuthenticationProvider(认证执行者):只负责判断某种特定的 Authentication 是否合法。
- AuthenticationManager(认证统筹管理器):总指挥官。它手里牵着一堆 AuthenticationProvider,负责转发请求。
在生产中,不管用户用什么方式登录,最终都会统一汇聚到 ProviderManager,它是 AuthenticationManager 的实现类,由它进行分发。多种方式认证整体架构图:

具体代码实现
业务控制器
UnifiedAuthController
1 |
|
OrderController:用于测试登陆之后的请求
1 |
|
LoginDTO
1 |
|
Authentication
EmailCodeAuthenticationToken
1 | import org.springframework.security.authentication.AbstractAuthenticationToken; |
EmailCodeAuthenticationProvider
1 | import jakarta.annotation.Resource; |
PasswordAuthenticationToken
1 | public class PasswordAuthenticationToken extends AbstractAuthenticationToken { |
PasswordAuthenticationProvider
1 |
|
SecurityConfig
1 | import org.springframework.context.annotation.Bean; |
统一的过滤器
1 | import io.jsonwebtoken.Claims; |